Skip to content

Add artifact relations tool and enrich fetch_artifacts with relation metadata#11

Open
sciapanCA wants to merge 1 commit intomainfrom
feature/mcp-relations-update
Open

Add artifact relations tool and enrich fetch_artifacts with relation metadata#11
sciapanCA wants to merge 1 commit intomainfrom
feature/mcp-relations-update

Conversation

@sciapanCA
Copy link
Copy Markdown
Contributor

Adds a new get_artifact_relations MCP tool and enriches fetch_artifacts with optional call relation metadata.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new get_artifact_relations tool to the CodeAlive MCP server, enabling exploration of call graphs, inheritance hierarchies, and symbol references. Additionally, the fetch_artifacts tool has been updated to include call relations in its XML output and wrap source code within a new <content> element. Review feedback focuses on enhancing the robustness of XML generation by ensuring that potential null values from API responses are handled safely before calling html.escape and that all XML attributes are consistently escaped to prevent malformed output.

Comment on lines +109 to +121
source_id = html.escape(data.get("sourceIdentifier", ""))
profile = html.escape(data.get("profile", ""))
found = data.get("found", False)

# Map profile back to MCP-friendly name
mcp_profile = profile
for mcp_name, api_name in PROFILE_MAP.items():
if api_name == profile:
mcp_profile = mcp_name
break

if not found:
return f'<artifact_relations sourceIdentifier="{source_id}" profile="{mcp_profile}" found="false"/>'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There are two issues in this block:

  1. Potential Crash: html.escape will raise an AttributeError if the API returns null for sourceIdentifier or profile, because dict.get(key, default) returns None if the key is present with a null value. Using data.get("key") or "" ensures a string is always passed.
  2. Logic Error: The profile mapping logic compares the escaped profile string with the raw api_name from PROFILE_MAP. If the profile name contains characters that require escaping (e.g., &), the comparison will fail. Mapping should occur on the raw value before escaping for XML.
Suggested change
source_id = html.escape(data.get("sourceIdentifier", ""))
profile = html.escape(data.get("profile", ""))
found = data.get("found", False)
# Map profile back to MCP-friendly name
mcp_profile = profile
for mcp_name, api_name in PROFILE_MAP.items():
if api_name == profile:
mcp_profile = mcp_name
break
if not found:
return f'<artifact_relations sourceIdentifier="{source_id}" profile="{mcp_profile}" found="false"/>'
raw_source_id = data.get("sourceIdentifier") or ""
raw_profile = data.get("profile") or ""
found = data.get("found", False)
# Map profile back to MCP-friendly name
mcp_profile = raw_profile
for mcp_name, api_name in PROFILE_MAP.items():
if api_name == raw_profile:
mcp_profile = mcp_name
break
source_id_attr = html.escape(raw_source_id)
profile_attr = html.escape(mcp_profile)
if not found:
return f'<artifact_relations sourceIdentifier="{source_id_attr}" profile="{profile_attr}" found="false"/>'

Comment on lines +145 to +154
attrs = [f'identifier="{html.escape(item.get("identifier", ""))}"']

file_path = item.get("filePath")
if file_path is not None:
attrs.append(f'filePath="{html.escape(file_path)}"')

start_line = item.get("startLine")
if start_line is not None:
attrs.append(f'startLine="{start_line}"')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to previous comments, html.escape will crash if identifier is null. Additionally, startLine should be escaped for XML robustness.

Suggested change
attrs = [f'identifier="{html.escape(item.get("identifier", ""))}"']
file_path = item.get("filePath")
if file_path is not None:
attrs.append(f'filePath="{html.escape(file_path)}"')
start_line = item.get("startLine")
if start_line is not None:
attrs.append(f'startLine="{start_line}"')
for item in group.get("items", []):
attrs = [f'identifier="{html.escape(item.get("identifier") or "")}"']
file_path = item.get("filePath")
if file_path is not None:
attrs.append(f'filePath="{html.escape(file_path)}"')
start_line = item.get("startLine")
if start_line is not None:
attrs.append(f'startLine="{html.escape(str(start_line))}"')

Comment on lines +183 to +192
call_id = html.escape(call.get("identifier", ""))
summary = call.get("summary")
if summary is not None:
call_elements.append(
f' <call identifier="{call_id}" summary="{html.escape(summary)}"/>'
)
else:
call_elements.append(f' <call identifier="{call_id}"/>')

parts.append(f' <{tag} count="{count}">')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Ensure that identifier is not null before passing it to html.escape to avoid an AttributeError. Also, escape the count attribute to ensure the generated XML is robust against unexpected API response types.

Suggested change
call_id = html.escape(call.get("identifier", ""))
summary = call.get("summary")
if summary is not None:
call_elements.append(
f' <call identifier="{call_id}" summary="{html.escape(summary)}"/>'
)
else:
call_elements.append(f' <call identifier="{call_id}"/>')
parts.append(f' <{tag} count="{count}">')
call_id = html.escape(call.get("identifier") or "")
summary = call.get("summary")
if summary is not None:
call_elements.append(
f' <call identifier="{call_id}" summary="{html.escape(summary)}"/>'
)
else:
call_elements.append(f' <call identifier="{call_id}"/>')
parts.append(f' <{tag} count="{html.escape(str(count))}">')

Comment on lines +134 to +142
total_count = group.get("totalCount", 0)
returned_count = group.get("returnedCount", 0)
truncated = str(group.get("truncated", False)).lower()

xml_parts.append(
f' <relation_group type="{html.escape(mcp_type)}" '
f'totalCount="{total_count}" returnedCount="{returned_count}" '
f'truncated="{truncated}">'
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block has robustness issues:

  1. If totalCount or returnedCount are null in the JSON response, group.get(..., 0) will return None, which might cause issues later or result in the string "None" in the XML.
  2. If truncated is null, str(None).lower() results in "none", which is not a valid XML boolean value (true/false).
  3. Attributes should be escaped to ensure the XML remains well-formed even if the API returns unexpected string values.
Suggested change
total_count = group.get("totalCount", 0)
returned_count = group.get("returnedCount", 0)
truncated = str(group.get("truncated", False)).lower()
xml_parts.append(
f' <relation_group type="{html.escape(mcp_type)}" '
f'totalCount="{total_count}" returnedCount="{returned_count}" '
f'truncated="{truncated}">'
)
total_count = group.get("totalCount") or 0
returned_count = group.get("returnedCount") or 0
truncated = str(bool(group.get("truncated"))).lower()
xml_parts.append(
f' <relation_group type="{html.escape(mcp_type)}" '
f'totalCount="{html.escape(str(total_count))}" returnedCount="{html.escape(str(returned_count))}" '
f'truncated="{truncated}">'
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant